package net.javlov.world.grid;

import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

import net.javlov.world.Body;
import net.javlov.world.CollisionEvent;

/**
 * Grid world implementation that can be used with bodies of any size and shape.
 * @author matthijs
 *
 */
public class GridWorldGeneric extends GridWorld {

	private List<CollisionEvent> collisionList;
	
	public GridWorldGeneric(int width, int height, double cellWidth,
			double cellHeight) {
		super(width, height, cellWidth, cellHeight);
		collisionList = new ArrayList<CollisionEvent>();
	}
	
	//TODO this only works correctly if no recursion is required, i.e. if there is
	//no collision with movable objects next to other objects. In the latter case,
	//all collision events might be fired before it can be guaranteed
	//that the move is allowed.
	/**
	 * @return true if the move was completed entirely (the number of cells indicated
	 * by the speed parameter), false otherwise.
	 */
	@Override
	public boolean translateBody(Body b, Direction d, int speed) {
		List<GridCell> currCells = getIntersectingCellsLarge(getTransformedShape(b)),
						targetCells = new ArrayList<GridCell>(currCells.size());
						
		GridCell locCell = grid.getCell(b.getX(), b.getY()),
				targetCell;
		
		int i;
		boolean moveAllowed;
		for ( i = 0; i < speed; i++) {
			moveAllowed = true;
			for ( GridCell currCell : currCells ) {
				targetCell = currCell.go(d);
				targetCells.add(targetCell);
				if ( targetCell.isBorder() || !move(b, d, targetCell) )
					moveAllowed = false;
			}
			if ( moveAllowed ) {
				fireQueuedCollisionEvents();
				for ( int j = 0; j < currCells.size(); j++ ) {
					currCells.get(j).removeBody(b);
					targetCells.get(j).addBody(b);
					currCells.set(j, targetCells.get(j));
				}
				locCell = locCell.go(d); //track body's location
			} else {
				cleanQueuedCollisionEvents();
				break;
			}
		}
		
		b.setLocation(locCell.getCenterX(), locCell.getCenterY());
		
		if ( i < speed-1 ) //move couldn't be entirely completed
			return false;
		return true;
	}
	
	/**
	 * Checks if the body can be moved to the specified target cell. If an obstacle is
	 * encountered in the target cell, the move is invalidated and a CollisionEvent is 
	 * fired immediately.
	 * If any other type of Body is encountered, a CollisionEvent is created and put in
	 * a list of collision events. These events will be fired if the move as a whole (as
	 * executed by {@link #translateBody(Body, Direction, int)}) is allowed (the move
	 * method only checks a single cell).
	 */
	@Override
	protected boolean move( Body b, Direction d, GridCell targetCell ) {
		//TODO Don't like all these fors and ifs
		List<Body> occupiers = targetCell.getOccupiers();
		//obstacles take precedence over all other bodies
		for ( Body targetBody : occupiers )
			if ( targetBody.getType() == Body.OBSTACLE ) {
				fireCollisionEvent(b, targetBody, new Point2D.Double(d.x(), d.y()));
				return false;
			}
		//next check if movables can be moved
		for ( Body targetBody : occupiers )
			if ( targetBody.getType() == Body.MOVABLE ) {
				queueCollisionEvent(b, targetBody, new Point2D.Double(d.x(), d.y()));
				if ( !translateBody(targetBody, d, 1) )
					return false;
			}
		//just queue collisionevent for the other body types
		for ( Body targetBody : occupiers )
			if ( targetBody.getType() != Body.OBSTACLE && targetBody.getType() != Body.MOVABLE )
				queueCollisionEvent(b, targetBody, new Point2D.Double(d.x(), d.y()));
				
		return true;
	}
	
	protected void queueCollisionEvent(Body b1, Body b2, Point2D.Double speed) {
		CollisionEvent e = new CollisionEvent(b1, b2, speed, (Point2D.Double)b2.getLocation());
		collisionList.add(e);
	}
	
	protected void fireQueuedCollisionEvents() {
		for ( CollisionEvent event : collisionList )
			fireCollisionEvent(event);
		cleanQueuedCollisionEvents();
	}
	
	protected void cleanQueuedCollisionEvents() {
		collisionList.clear();
	}
	
	protected Shape getTransformedShape(Body b) {
		//rotate figure
		AffineTransform at = new AffineTransform();
		//rotate around the body's center
		at.rotate(b.getBearing(), b.getX(), b.getY());
		return at.createTransformedShape(b.getFigure());
	}
	
	@Override
	protected void addToCells(Body b) {
		int count = 0;
    	for ( GridCell c : getIntersectingCellsLarge(getTransformedShape(b)) ) {
    		count++;
    		c.addBody(b);
    	}
    }
	
	@Override
	protected void removeFromCells(Body b) {
		for ( GridCell c : getIntersectingCellsLarge(getTransformedShape(b)) )
    		c.removeBody(b);
    }
}
